@scalewing/react 1.40.0 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +274 -7
  3. package/dist/calendar-labels.d.ts +2 -0
  4. package/dist/calendar-labels.js +7 -0
  5. package/dist/components/Article.d.ts +60 -0
  6. package/dist/components/Article.js +42 -0
  7. package/dist/components/BarChart.d.ts +20 -4
  8. package/dist/components/BarChart.js +13 -6
  9. package/dist/components/ButtonGroup.d.ts +8 -1
  10. package/dist/components/ButtonGroup.js +11 -2
  11. package/dist/components/DateField.d.ts +14 -2
  12. package/dist/components/DateField.js +14 -11
  13. package/dist/components/DenominationGrid.d.ts +3 -2
  14. package/dist/components/DenominationGrid.js +21 -3
  15. package/dist/components/Dialog.d.ts +29 -13
  16. package/dist/components/Dialog.js +22 -20
  17. package/dist/components/DocumentViewer.d.ts +41 -0
  18. package/dist/components/DocumentViewer.js +72 -0
  19. package/dist/components/Field.d.ts +13 -2
  20. package/dist/components/Field.js +14 -38
  21. package/dist/components/FileField.d.ts +44 -0
  22. package/dist/components/FileField.js +66 -0
  23. package/dist/components/FilterChips.d.ts +7 -37
  24. package/dist/components/FilterChips.js +32 -24
  25. package/dist/components/ListRow.d.ts +33 -3
  26. package/dist/components/ListRow.js +18 -17
  27. package/dist/components/MarkdownView.d.ts +39 -0
  28. package/dist/components/MarkdownView.js +30 -0
  29. package/dist/components/NavCount.d.ts +15 -2
  30. package/dist/components/NavCount.js +16 -6
  31. package/dist/components/SegmentedControl.d.ts +17 -1
  32. package/dist/components/SegmentedControl.js +11 -3
  33. package/dist/components/Switch.d.ts +16 -1
  34. package/dist/components/Switch.js +17 -6
  35. package/dist/components/action-bar/ActionBarSummary.d.ts +10 -2
  36. package/dist/components/action-bar/ActionBarSummary.js +11 -2
  37. package/dist/components/chart/use-chart-width.js +2 -24
  38. package/dist/components/date-field/CalendarEntryButton.d.ts +24 -0
  39. package/dist/components/date-field/CalendarEntryButton.js +16 -0
  40. package/dist/components/date-field/TypedEntry.d.ts +26 -0
  41. package/dist/components/date-field/TypedEntry.js +13 -0
  42. package/dist/components/denomination-grid/CountCell.d.ts +16 -0
  43. package/dist/components/denomination-grid/CountCell.js +14 -0
  44. package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -1
  45. package/dist/components/denomination-grid/DenominationStrip.js +2 -2
  46. package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
  47. package/dist/components/denomination-grid/EntryCell.js +2 -2
  48. package/dist/components/denomination-grid/StripRow.js +1 -10
  49. package/dist/components/denomination-grid/TransposedStrip.d.ts +17 -0
  50. package/dist/components/denomination-grid/TransposedStrip.js +43 -0
  51. package/dist/components/denomination-grid/types.d.ts +31 -0
  52. package/dist/components/denomination-grid/types.js +7 -1
  53. package/dist/components/denomination-grid/validate-options.d.ts +7 -0
  54. package/dist/components/denomination-grid/validate-options.js +27 -0
  55. package/dist/components/dialog/DialogFooter.d.ts +6 -5
  56. package/dist/components/dialog/DialogFooter.js +6 -6
  57. package/dist/components/dialog/validate.d.ts +24 -0
  58. package/dist/components/dialog/validate.js +23 -0
  59. package/dist/components/document-viewer/document-viewer-labels.d.ts +23 -0
  60. package/dist/components/document-viewer/document-viewer-labels.js +23 -0
  61. package/dist/components/document-viewer/page-box.d.ts +8 -0
  62. package/dist/components/document-viewer/page-box.js +12 -0
  63. package/dist/components/document-viewer/pdf-document.d.ts +32 -0
  64. package/dist/components/document-viewer/pdf-document.js +66 -0
  65. package/dist/components/document-viewer/pdf-options.d.ts +36 -0
  66. package/dist/components/document-viewer/pdf-options.js +46 -0
  67. package/dist/components/document-viewer/render-pdf-page.d.ts +20 -0
  68. package/dist/components/document-viewer/render-pdf-page.js +38 -0
  69. package/dist/components/document-viewer/scroll-chain.d.ts +6 -0
  70. package/dist/components/document-viewer/scroll-chain.js +23 -0
  71. package/dist/components/document-viewer/use-error-report.d.ts +5 -0
  72. package/dist/components/document-viewer/use-error-report.js +13 -0
  73. package/dist/components/document-viewer/use-page-render.d.ts +21 -0
  74. package/dist/components/document-viewer/use-page-render.js +70 -0
  75. package/dist/components/document-viewer/use-pdf-document.d.ts +22 -0
  76. package/dist/components/document-viewer/use-pdf-document.js +47 -0
  77. package/dist/components/document-viewer/use-pinch-zoom.d.ts +12 -0
  78. package/dist/components/document-viewer/use-pinch-zoom.js +67 -0
  79. package/dist/components/document-viewer/use-zoom.d.ts +23 -0
  80. package/dist/components/document-viewer/use-zoom.js +70 -0
  81. package/dist/components/document-viewer/zoom.d.ts +29 -0
  82. package/dist/components/document-viewer/zoom.js +37 -0
  83. package/dist/components/field/field-checks.d.ts +34 -0
  84. package/dist/components/field/field-checks.js +42 -0
  85. package/dist/components/file-field/file-field-checks.d.ts +13 -0
  86. package/dist/components/file-field/file-field-checks.js +21 -0
  87. package/dist/components/file-field/use-file-drop.d.ts +34 -0
  88. package/dist/components/file-field/use-file-drop.js +78 -0
  89. package/dist/components/file-field/use-swap-focus.d.ts +15 -0
  90. package/dist/components/file-field/use-swap-focus.js +40 -0
  91. package/dist/components/filter-chips/FilterChip.d.ts +16 -0
  92. package/dist/components/filter-chips/FilterChip.js +20 -0
  93. package/dist/components/filter-chips/options.d.ts +75 -0
  94. package/dist/components/filter-chips/options.js +11 -0
  95. package/dist/components/filter-chips/validate.d.ts +9 -0
  96. package/dist/components/filter-chips/validate.js +48 -0
  97. package/dist/components/list/ListRowContent.d.ts +8 -2
  98. package/dist/components/list/ListRowContent.js +6 -3
  99. package/dist/components/list/ListRowItem.d.ts +10 -0
  100. package/dist/components/list/ListRowItem.js +17 -0
  101. package/dist/components/list/row-naming.d.ts +17 -0
  102. package/dist/components/list/row-naming.js +19 -0
  103. package/dist/components/list/row-slot.d.ts +8 -0
  104. package/dist/components/list/row-slot.js +12 -0
  105. package/dist/components/markdown/markdown-limits.d.ts +14 -0
  106. package/dist/components/markdown/markdown-limits.js +14 -0
  107. package/dist/components/markdown/markdown-model.d.ts +94 -0
  108. package/dist/components/markdown/markdown-model.js +8 -0
  109. package/dist/components/markdown/markdown-options.d.ts +18 -0
  110. package/dist/components/markdown/markdown-options.js +10 -0
  111. package/dist/components/markdown/markdown-rules.d.ts +28 -0
  112. package/dist/components/markdown/markdown-rules.js +81 -0
  113. package/dist/components/markdown/markdown-text.d.ts +17 -0
  114. package/dist/components/markdown/markdown-text.js +86 -0
  115. package/dist/components/markdown/parse-markdown.d.ts +4 -0
  116. package/dist/components/markdown/parse-markdown.js +21 -0
  117. package/dist/components/markdown/read-blocks.d.ts +8 -0
  118. package/dist/components/markdown/read-blocks.js +168 -0
  119. package/dist/components/markdown/read-inlines.d.ts +12 -0
  120. package/dist/components/markdown/read-inlines.js +126 -0
  121. package/dist/components/markdown/read-markdown.d.ts +9 -0
  122. package/dist/components/markdown/read-markdown.js +27 -0
  123. package/dist/components/markdown/reading-context.d.ts +18 -0
  124. package/dist/components/markdown/reading-context.js +35 -0
  125. package/dist/components/markdown/render-markdown.d.ts +10 -0
  126. package/dist/components/markdown/render-markdown.js +52 -0
  127. package/dist/components/use-element-width.d.ts +11 -0
  128. package/dist/components/use-element-width.js +38 -0
  129. package/dist/css/chart-tones.d.ts +2 -0
  130. package/dist/css/chart-tones.js +5 -1
  131. package/dist/css/css-article.d.ts +2 -0
  132. package/dist/css/css-article.js +110 -0
  133. package/dist/css/css-bar-chart.d.ts +17 -2
  134. package/dist/css/css-bar-chart.js +38 -7
  135. package/dist/css/css-button-group-attached.d.ts +3 -0
  136. package/dist/css/css-button-group-attached.js +18 -1
  137. package/dist/css/css-canvas-grouped.d.ts +17 -0
  138. package/dist/css/css-canvas-grouped.js +40 -0
  139. package/dist/css/css-components.js +18 -0
  140. package/dist/css/css-count-pill.js +14 -1
  141. package/dist/css/css-date-field-calendar-entry.d.ts +11 -0
  142. package/dist/css/css-date-field-calendar-entry.js +50 -0
  143. package/dist/css/css-date-field.js +4 -0
  144. package/dist/css/css-denomination-grid.js +4 -0
  145. package/dist/css/css-denomination-transposed.d.ts +4 -0
  146. package/dist/css/css-denomination-transposed.js +178 -0
  147. package/dist/css/css-dialog-canvas.d.ts +3 -0
  148. package/dist/css/css-dialog-canvas.js +20 -0
  149. package/dist/css/css-dialog-frame.js +19 -2
  150. package/dist/css/css-dialog-grabber.d.ts +4 -0
  151. package/dist/css/css-dialog-grabber.js +56 -0
  152. package/dist/css/css-dialog.js +3 -2
  153. package/dist/css/css-document-viewer.d.ts +2 -0
  154. package/dist/css/css-document-viewer.js +186 -0
  155. package/dist/css/css-document.js +3 -0
  156. package/dist/css/css-field-width.d.ts +12 -0
  157. package/dist/css/css-field-width.js +49 -0
  158. package/dist/css/css-field.js +4 -0
  159. package/dist/css/css-file-field.d.ts +2 -0
  160. package/dist/css/css-file-field.js +180 -0
  161. package/dist/css/css-filter-chips-layout.d.ts +2 -0
  162. package/dist/css/css-filter-chips-layout.js +42 -0
  163. package/dist/css/css-filter-chips.js +4 -0
  164. package/dist/css/css-list-row-trailing.d.ts +2 -0
  165. package/dist/css/css-list-row-trailing.js +91 -0
  166. package/dist/css/css-list-row.d.ts +6 -0
  167. package/dist/css/css-list-row.js +19 -3
  168. package/dist/css/css-list.js +5 -1
  169. package/dist/css/css-markdown.d.ts +2 -0
  170. package/dist/css/css-markdown.js +144 -0
  171. package/dist/css/css-segmented-sizing.d.ts +13 -0
  172. package/dist/css/css-segmented-sizing.js +66 -0
  173. package/dist/css/css-segmented.js +4 -0
  174. package/dist/css/css-switch.d.ts +6 -0
  175. package/dist/css/css-switch.js +39 -0
  176. package/dist/css/css-variables.js +1 -0
  177. package/dist/css/stacking.d.ts +1 -0
  178. package/dist/css/stacking.js +4 -1
  179. package/dist/css/stylesheet.js +12 -0
  180. package/dist/date-field-labels.d.ts +5 -0
  181. package/dist/date-field-labels.js +1 -0
  182. package/dist/document-viewer-worker.d.ts +9 -0
  183. package/dist/document-viewer-worker.js +9 -0
  184. package/dist/index.d.ts +15 -9
  185. package/dist/index.js +13 -7
  186. package/dist/list-options.d.ts +11 -7
  187. package/dist/list-options.js +20 -8
  188. package/dist/styles.css +889 -44
  189. package/dist/theme/ThemeProvider.d.ts +11 -1
  190. package/dist/theme/ThemeProvider.js +4 -2
  191. package/package.json +10 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.42.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5218882: `Article`: a page that reads like a post, on a card (`variant`, as `Card`'s) no wider than the new `--sw-reading-max` (46rem): an optional `cover` photo (`{ src, alt }` or `null`) edge to edge across the top, the `eyebrow` with the consumer's `actions` at its row's end, the `title` in heading type (`titleAs` `h1`, `h2` or `h3`; it names the `article` landmark, which the ref reaches), the muted `meta` line, a hairline and the body. New exports `Article`, `ArticleProps`, `ArticleCover`, `ArticleTitleElement`, `articleTitleElements`; new `sw-article*` classes. No new tokens, no new dependency. Request: `docs/requests/teisoro-article.md`.
8
+ - 6befd6c: `DocumentViewer`: a PDF drawn from its bytes (never a URL) one page at a time with PDF.js, fitted to its width, with 48-point previous and next steps around "Page n of m" in a polite live region, pinch and `+`/`-`/`0` zoom, a transparent text layer for assistive technology and selection, the consumer's loading and failed words and actions, and no download or print of its own. New exports `DocumentViewer`, `DocumentViewerProps`, `DocumentViewerLabels`; new export path `@scalewing/react/document-viewer-worker` (the PDF.js worker entry, bundled by the consumer, such as Vite's `?worker&url`); new `sw-document-viewer*` classes.
9
+
10
+ New production dependency: `pdfjs-dist` 6.4.299, pinned exactly. Rationale: drawing PDFs needs a full parser and renderer, and PDF.js (Mozilla, Apache-2.0) is the maintained browser-grade one, at the version a server checking the same uploads can pin too. It is loaded with a dynamic import only when a viewer opens a document, its legacy build is used for older browsers, and it runs hardened: nothing fetched beyond the bytes, no eval, no XFA, no scripting. Its optional `@napi-rs/canvas` (Node only, 28 to 35 MB per platform) is never loaded in a browser and can be skipped with pnpm's `ignoredOptionalDependencies`; `pdfjs-dist` asks for Node 22.13 or later. Request: `docs/requests/teisoro-document-viewer.md`.
11
+
12
+ - d387481: `FileField`: one file chosen with the system picker or dropped on the field, with `Field`'s label, description and error. Empty, a wide zone named by the label and `chooseLabel` (`dropLabel` after it where the pointer can drag); chosen (`value={{ name, detail }}`), the file's name and detail with the consumer's `icon` and a `changeLabel` button. `onChange` receives the `File`; nothing is read or uploaded, and a dropped file is not filtered by `accept`. New exports `FileField`, `FileFieldProps`, `FileFieldValue`; new `sw-file-field*` classes. No new tokens, no new dependency. Request: `docs/requests/teisoro-file-field.md`.
13
+ - 33a6d6e: `ListRow` `unread` / `unreadLabel`, `tag` and `action`: an accent dot after the title for a row the person has not opened, read as `unreadLabel` after the title (required while `unread`); a short status such as a `Badge` before the row's end, describing a pressable row; and a control such as an `ActionMenu` after the row, outside its press area, in the same list item. New classes `sw-list-row-unread`, `sw-list-row-tag`, `sw-list-item-action`, `sw-list-row-action`; an unread row without words throws a `RangeError`. Existing rows are unchanged, except that an empty `leading` (`null`, `false` or `''`, as `leading={isLate && <Dot />}` gives) no longer draws an empty mark box and its gap before the title. No new tokens, no new dependency. Request: `docs/requests/teisoro-list-row-trailing.md`.
14
+ - f027566: `MarkdownView` (`newTabLabel` required): Markdown drawn in Scalewing's type as React elements, never as HTML from a string: paragraphs, headings (`#` as an `h2`, with ids from their words and an optional `idPrefix`), lists, bold, italic, links (`http`, `https` and `mailto` only, read as the browser would, in a new tab with `rel="noopener noreferrer"`; reference links and autolinks too; any other scheme shows as plain words), block quotes as a highlighted box, line breaks, thematic breaks and `![caption](photo:<id>)` photos the consumer resolves (`resolveImage`) to a safe address, with words (their alt or caption) and captions. Raw HTML, scripts, tables, other images and unresolved or wordless photos are left out, code is shown as plain text, and quotes, lists and marks nested past 16 levels are shown as their words; the reading is linear, so a realistic 50,000-character page reads in about 7 ms and the slowest crafted one in about a tenth of a second. `inspectMarkdown(source, options)` returns the `headings` (for an "On this page" list), the first 100 `omissions` and `omissionCounts` (for an editor to say what will not be shown), refusing what the view refuses. New exports `MarkdownView`, `MarkdownViewProps`, `MarkdownOptions`, `inspectMarkdown`, `MarkdownInspection`, `MarkdownHeading`, `MarkdownImage`, `MarkdownImageResolver`, `MarkdownOmission`, `MarkdownOmissionKind`, `MarkdownOmissionCounts`, `markdownOmissionKinds`; new `sw-markdown*` classes. New production dependency, pinned exactly: `markdown-it` 14.3.2 (MIT; a CommonMark parser to tokens, linear in the text with capped nesting; its HTML renderer is never used), about 53 kB gzip, loaded only by apps that import the view. Request: `docs/requests/teisoro-markdown-view.md`.
15
+ - 8f70607: `NavCount` `tone?: 'danger' | 'accent'` (default `danger`): `accent` draws the count pill in the accent colour with its on-colour figures, for a count that is news (what was added since the person last looked) rather than work waiting. New exports `navCountTones`, `NavCountTone`; new class `sw-nav-count-accent`; an unknown tone throws a `RangeError`. Existing counts are unchanged. No new tokens, no new dependency. Request: `docs/requests/teisoro-nav-count-tone.md`.
16
+
17
+ ## 1.41.0
18
+
19
+ ### Minor Changes
20
+
21
+ - f6bce98: `BarChart`: an item `tone="neutral"` (the muted grey, 3:1 against the track and the surface in every palette; `barChartTones` gains `neutral`) and `appearance="bare"` (`BarChartAppearance`, `barChartAppearances`; default `track`), the bars alone with no track and no value axis. An unknown appearance throws a `RangeError`. New class `sw-bar-chart-bare`; no new tokens or dependencies. Request: `docs/requests/teisoro-bar-chart-bare.md`.
22
+ - 2f34b46: `ThemeProvider` `canvas="grouped"` (`ThemeCanvas`, `themeCanvases`; default `default`, unchanged): an opt-in grey page tone so white cards stand apart, written as `data-canvas="grouped"` on the themed node. The generated canvas declares `--sw-canvas-grouped` (light: the background with 7 % of the muted colour; dark: the background) and paints it only where a canvas asks for it. The text and muted colours keep 4.5:1 on it in every palette; keep links and accent words in the cards. An unknown value throws a `RangeError`. New public CSS hooks: the `data-canvas` attribute and the `--sw-canvas-grouped` custom property. No new tokens, classes or dependencies. Request: `docs/requests/teisoro-canvas-grouped.md`.
23
+ - 9c6078b: `DateField` `entry="calendar"` (`DateFieldEntry`, `dateFieldEntries`; default `typed`, unchanged): a day-picker field, one neutral field-shaped `Button` showing the calendar glyph and the chosen day in the locale's medium style, or `labels.chooseDate` while empty, that opens the same calendar. It is named by the label and its words and described by `description` and `error`; a required one is described by the new `labels.required` ("Required"). The consumer's `error` reports a missing day. An unknown entry throws a `RangeError`. New classes `sw-date-field-calendar-entry`, `sw-date-field-calendar-entry-text` and `sw-date-field-calendar-entry-invalid`; no new tokens or dependencies. Request: `docs/requests/teisoro-date-field-calendar-entry.md`.
24
+ - 995cb76: `DenominationGrid`: a row's `negativeAppearance="plain"` (`DenominationGridNegativeAppearance`, `denominationNegativeAppearances`; default `danger`) sets its negative counts in the text colour with their minus, for counts going out by design; `transposeBelow` (`'md' | 'lg'`, `DenominationTransposeBreakpoint`, `denominationTransposeBreakpoints`; strip only) draws the strip below that width with a row per denomination and a column per grid row, entry, value lines and totals kept; `countDigits` (3 to 6, entry rows only) keeps that many digits' room in entry counts. A signed count's cell also takes `sw-denomination-cell-signed`. Unknown values, `transposeBelow` on tiles and `countDigits` out of range or without entry rows throw a `RangeError`. New classes `sw-denomination-cell-signed`, `sw-denomination-cell-plain`, `sw-denomination-count-digits` and the `sw-denomination-transposed*` family; no new tokens or dependencies. Request: `docs/requests/teisoro-denomination-grid-board.md`.
25
+ - 9ad24d3: `Dialog` gets three options:
26
+
27
+ - `status` (a line beside the actions, in the footer's polite live region);
28
+ - `canvas="grouped"` (`ThemeCanvas`; the grouped canvas grey fill, solid, so cards stand apart);
29
+ - `sheetGrabber` (a decorative grabber on the bottom sheet; needs `sheetBelow`).
30
+
31
+ An `ActionBar` or `Dialog` `summary` whose `value` is empty no longer throws. It means there is no figure yet: the line is left out and the live region stays, so the first figure is still announced.
32
+
33
+ New classes: `sw-dialog-footer-note`, `sw-dialog-canvas-grouped`, `sw-dialog-grabber` and `sw-dialog-has-grabber`. New stacking layer `sheetGrabber`. No new tokens or dependencies. Request: `docs/requests/teisoro-dialog-board.md`.
34
+
35
+ - 594a9c7: `Field` `width` (`FieldWidth`, `fieldWidths`: `xs` 4rem, `sm` 6rem, `md` 8rem, `lg` 12rem, `xl` 16rem, the Table column sizes): caps the control (or an adorned or search field's frame) at that width at the column's start, never wider than the column, so a short value does not stretch across a form; the label and messages keep the column. Left out, nothing changes. It needs one native text control or select child (a `TypeError` otherwise); an unknown width throws a `RangeError`. New classes `sw-field-sized` and `sw-field-width-*`; no new tokens or dependencies. Request: `docs/requests/teisoro-field-width.md`.
36
+ - 74d6db2: `FilterChips` gains four opt-in options: `labelVisuallyHidden` (the label stays the group's name, not drawn); `countAppearance="inline"` (`FilterChipCountAppearance`, `filterChipCountAppearances`; default `figure`), the count in the chip's words after a hidden middle dot; `overflow="scroll"` (`FilterChipsOverflow`, `filterChipsOverflows`; default `wrap`), one row that scrolls sideways inside its own box; and `multiple` with a `value` list and `onChange(values)`, each chip a checkbox and its own Tab stop (a multiple group cannot be `required`). Unknown values, a multiple value that is not a list or holds a value that is no option, and `required` with `multiple` throw a `RangeError`. New classes `sw-filter-chip-count-inline` and `sw-filter-chips-scroll`; no new tokens or dependencies. Request: `docs/requests/teisoro-filter-chips-board.md`.
37
+ - 7cd729f: `SegmentedControl` `fullWidthBelow` and an attached `ButtonGroup` `fullWidthBelow` (the layout `Breakpoint`, `md`): below it the control fills its column, its segments or buttons sharing the width equally; from it up nothing changes. `SegmentedControl` `size="sm"` (`SegmentedSize`, `segmentedSizes`; default `md`) pads each segment a step less and keeps its label on one line with an ellipsis past its share. Unknown values, and `fullWidthBelow` on a group that is not attached, throw a `RangeError`. New classes `sw-segmented-field-full-below-md`, `sw-segmented-sm` and `sw-button-group-full-below-md`; no new tokens or dependencies. Request: `docs/requests/teisoro-full-width-controls.md`.
38
+ - a044de3: `ListRow` `valueTone="warning"` (the warning colour's ink, halfway to the text colour, 4.5:1 on the surface in every palette; `listRowValueTones` gains `warning`) and `valueBadge` (a `BadgeTone`: the value drawn as a Badge at the row's end, still in the row's name; needs a value, never with `valueTone`). New class `sw-list-row-value-warning`; no new tokens or dependencies. The refusal for a value option without a value now reads "ListRow valueDetail, valueTone and valueBadge need a value". Request: `docs/requests/teisoro-list-row-value.md`.
39
+ - 0d0b1bd: `Switch` `layout="trailing"` (`SwitchLayout`, `switchLayouts`; default `leading`): the words first and the control at the end of a full-width row; and `checkedTone="success"` (`SwitchCheckedTone`, `switchCheckedTones`; default `accent`): the success track while on, `Highlight` in forced colours. Unknown values throw a `RangeError`. New classes `sw-switch-trailing` and `sw-switch-success`; no new tokens or dependencies. Request: `docs/requests/teisoro-switch-trailing.md`.
40
+
3
41
  ## 1.40.0
4
42
 
5
43
  ### Minor Changes
package/README.md CHANGED
@@ -32,11 +32,14 @@ import {
32
32
  DescriptionItem,
33
33
  ListGroup,
34
34
  ListRow,
35
+ MarkdownView,
35
36
  DenominationGrid,
36
37
  FilterChips,
37
38
  Grid,
38
39
  DateField,
39
40
  Field,
41
+ FileField,
42
+ Article,
40
43
  Stepper,
41
44
  Steps,
42
45
  Step,
@@ -55,6 +58,16 @@ import {
55
58
 
56
59
  `colorScheme` is `"light"`, `"dark"`, or `"system"` so product users can switch. Each named palette already has a light pair and a dark pair. Brand overlays may be a flat `colors` map or `{ light, dark }` so both schemes are declared once.
57
60
 
61
+ `canvas="grouped"` (opt-in; `themeCanvases`, default `"default"`) sets the page on a quiet grey so white cards stand apart, as a report or a settings page that groups its content in cards: in the light scheme the background with a little of the muted colour, in the dark scheme the background itself, which is already darker than the surface. It writes `data-canvas="grouped"` on the themed node (set the attribute yourself on a `data-theme` node without `ThemeProvider`); a nested `ThemeProvider canvas="grouped"` paints a grouped region. The text and muted colours keep 4.5:1 or more on it in every palette.
62
+
63
+ The accent does not keep 4.5:1 on the grey in every palette, so keep links and accent words in the cards. An unknown value throws a `RangeError`.
64
+
65
+ ```tsx
66
+ <ThemeProvider canvas="grouped" colorScheme="system">
67
+ <Card padding={4}>North colony</Card>
68
+ </ThemeProvider>
69
+ ```
70
+
58
71
  ```html
59
72
  <div class="sw-padding-top-4 sw-padding-x-4"></div>
60
73
  ```
@@ -87,7 +100,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
87
100
 
88
101
  `Field` supports optional `description`, `error`, and `required` on one native input, select, or textarea child. It preserves an existing `aria-describedby` and appends a stable ID for the hint, or for the error, which replaces the hint on screen while it is supplied. `error` sets `aria-invalid` and token-owned invalid styling. The error sits in a polite live region (`aria-live="polite"`) that is always rendered, empty while there is no error, so an error that appears while the person types, or after a submit, is announced once, politely, when its text is swapped in; it is never `role="alert"`, so a submit that finds several errors does not fire several assertive alerts at once. Empty, the region takes no room in the layout. The consumer owns validation and localized messages, and after a refused submit it moves focus to the first invalid control (or to one form-level summary). `Checkbox`, `RadioGroup` and `DateField` expose their `error` the same way. `invalid` marks the control invalid the same way (`aria-invalid` and the danger border, `Mark` in forced colors) without a message of its own, for fields whose one error is shown elsewhere, such as a count that adds up to nothing under a group of count fields: point each control's `aria-describedby` at that message so it is read with the field. The field's own polite region stays empty and its `description` stays on screen; `error` implies `invalid`. Passing `invalid`, even `invalid={false}` but not `invalid={undefined}`, needs one native control child and throws a `TypeError` on the first render otherwise, so a form learns it while the fields are valid, not on its first failed submit. `error` keeps its rule: it needs the native control only while it holds a message. `invalid` is `Field`'s only: `DateField`, `Select` and the other controls with `error` do not have it yet.
89
102
 
90
- `requiredMark="hidden"` (default `"visible"`; `requiredMarks`, `RequiredMark`) leaves out the label's `aria-hidden` " *" on a required `Field`, `Select`, `DateField`, `Combobox` or `FilterChips`, for a form that marks nothing as required (or marks the optional fields instead). The control stays required (native `required` or `aria-required`), so assistive technology still hears it; say in the form's words which fields must be filled. An unknown value throws a `RangeError`.
103
+ `requiredMark="hidden"` (default `"visible"`; `requiredMarks`, `RequiredMark`) leaves out the label's `aria-hidden` " *" on a required `Field`, `Select`, `DateField`, `Combobox` or `FilterChips`, for a form that marks nothing as required (or marks the optional fields instead). The control stays required (native `required` or `aria-required`, or, on a `DateField` with `entry="calendar"`, its `labels.required` word), so assistive technology still hears it; say in the form's words which fields must be filled. An unknown value throws a `RangeError`.
91
104
 
92
105
  `Field` `changed` marks a value the person changed from a saved one, such as a field in a correction: the control's border, or an adorned field's frame, takes the accent color a hairline thicker (an inset shadow, so nothing moves; `Highlight` in forced colors), and its fill takes a 12 % accent tint over its own background, so a changed field reads apart from the focused one, whose ring never tints. The value keeps 4.5:1 or more on the tint in every palette; the tint is on text controls and selects (not a range, color or file input), a select's chevron stays where its size puts it, while a placeholder shows the fill stays plain, on the tint an adorned field's prefix and suffix take the text color, and forced colors drop the tint. It is a cue beside the words, so say what the value was in `description`; it sets no ARIA state, and `invalid` and `error` win over it. Like `invalid`, passing it, even `false` but not `undefined`, needs one native control child from the first render (a `TypeError` otherwise).
93
106
 
@@ -113,6 +126,14 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
113
126
 
114
127
  `Field` `prefix` and `suffix` put short text such as `$` or `%` inside the frame of one native `<input>` child, before or after the value. The text is not part of the value; the input is named by its label plus the adornment (`"Drop amount $"`), unless it names itself with its own `aria-label` or `aria-labelledby`, in which case the adornment joins its description. A press anywhere on the frame focuses the input. Formatting the value stays with the consumer. Any other child throws a `TypeError`.
115
128
 
129
+ `Field` `width` (opt-in; `fieldWidths`) caps the control at a Table column size, `xs` 4rem, `sm` 6rem, `md` 8rem, `lg` 12rem or `xl` 16rem, so a short value such as a rate, a count or a code sits in a box its own size instead of stretching across the form's column. The cap is on the control alone (an adorned or search field's frame), placed at the column's start and never wider than the column on a narrow screen; the label, description and error keep the column's width. It caps a text input (the canvas's typed inputs: text, email, number, search, url, password or no type), a select or a textarea (a checkbox, a range or another input type keeps its own size), and a sized select keeps its control height. It needs one native control child, as `prefix` does (a `TypeError` otherwise); an unknown width throws a `RangeError`.
130
+
131
+ ```tsx
132
+ <Field label="Body length" suffix="cm" width="sm">
133
+ <input inputMode="decimal" />
134
+ </Field>
135
+ ```
136
+
116
137
  `Field` `variant="search"` (default `"outlined"`; `fieldVariants`) is a search box over one native `<input>` child with a `value`: the `label` is not drawn but still names the input, which becomes `type="search"` (a `searchbox`); `leading` is your glyph at the frame's start, such as a magnifier, hidden from assistive technology; and while the input holds text and is enabled and not read-only, a clear button named `clearLabel` ("Clear search") ends the frame, puts focus back in the input and calls `onClear` (set the value to `''`); Escape with text calls `onClear` too. The browser's own clear button is hidden, so there is one. The input gets `enterKeyHint="search"` unless you pass one. The frame is the adorned field's, so focus, `error`, `description`, `invalid` and the disabled look work as on any field; a press on the glyph focuses the input; the clear button is the 44 px target on a coarse pointer. A search field without a nonempty `clearLabel` or `onClear`, with `prefix` or `suffix`, or with a child that is not an `<input>` with a `value` throws; `leading`, `clearLabel` and `onClear` on an outlined field, or an unknown `variant`, throw too.
117
138
 
118
139
  ```tsx
@@ -121,6 +142,23 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
121
142
  </Field>
122
143
  ```
123
144
 
145
+ `FileField` is one file chosen with the system picker or dropped on the field, with `Field`'s `label` (above it, or hidden with `labelVisuallyHidden`), `description` and `error` (a polite live region in place of the hint, the frame's edge in the danger color, `Mark` in forced colors). Empty, it is one wide zone on the subtle fill, a button named by the label and `chooseLabel` ("Field guide Choose a PDF"), with `dropLabel` ("or drop it here") muted after it where the pointer can drag (left out on a coarse pointer). Chosen (`value={{ name, detail? }}`), it shows the consumer's `icon` (hidden from assistive technology), the file's name over the muted `detail` and a neutral button named by the label and `changeLabel`, described by the name and detail. Either button opens the picker (the native input is hidden and out of the Tab order, its `accept` the list the picker offers); `onChange` receives the `File` chosen or dropped, the first of several, and when a choice or a drop swaps the zone and the chosen row while focus is in the field, focus moves to the button now there (focus the person moved elsewhere is never taken back). A file dragged over the field draws the accent edge and tint, its words in the text color. It never reads, checks or uploads the file, and a dropped file is not filtered by `accept`: check what you receive (type, size, its header) and say why in `error`, keeping the earlier `value` or none. `disabled` blocks the picker, refuses a dropped file and dashes the frame. `id` prefixes the ids of the field's parts. An empty `label`, `chooseLabel` or `changeLabel`, a blank `dropLabel`, or a `value` with no name or a blank detail throws a `RangeError`.
146
+
147
+ ```tsx
148
+ <FileField
149
+ label="Field guide"
150
+ description="A PDF up to 25 MB"
151
+ accept=".pdf,application/pdf"
152
+ chooseLabel="Choose a PDF"
153
+ dropLabel="or drop it here"
154
+ changeLabel="Choose another"
155
+ icon={<FileText />}
156
+ value={file ? { name: file.name, detail: `${pages} pages · ${size}` } : null}
157
+ error={error}
158
+ onChange={(next) => checkPdf(next).then(setFile, (why) => setError(why))}
159
+ />
160
+ ```
161
+
124
162
  `Stepper` is a whole-number count changed with − and + or typed. The count is a native text input with `role="spinbutton"` and `inputmode="numeric"`, named by `label` (visible above it, or `labelVisuallyHidden` where table headers say it) and the one tab stop: ArrowUp and ArrowDown step it by `step` (default 1), Home goes to `min` and End to a finite `max` (a range below zero takes `inputmode="text"`, since a phone's numeric pad has no minus key). The − and + buttons are pointer targets outside the tab order, named by `decrementLabel` and `incrementLabel` (your copy), and a press on one leaves focus where it was; the count and both buttons sit in a `group` named by the label. − is disabled at `min` (default 0) and + at `max` (no upper bound when left out). `value` is a count or `null` for an empty box, which the buttons treat as `min` and `placeholder` can fill. Typed text is refused unless it is digits (with a leading minus when `min` is below zero), so `onChange` only receives a whole number or `null`; a typed count is reported as typed even outside the bounds, so say why in `error` (a polite message, `aria-invalid` and the danger border, as `Field`'s), and the buttons then move it back inside them. `aria-describedby` takes ids of elements that describe the count, such as the value it adds up to, read before an `error`'s message. `changed` takes `Field`'s changed look; `readOnly` keeps the count focusable and hides the buttons in their own room; `disabled` stops all three. `limitAppearance="plain"` (default `"faded"`) keeps − at `min` and + at `max` in their usual look instead of faded, for a count sheet where a faded − reads as broken; they are still disabled, a disabled or read-only Stepper is unchanged, forced colors still grey them, and an unknown value throws a `RangeError`. The count is a few digits wide and keeps that width (`width="fixed"`), or fills its container (`width="full"`, for a table cell). Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
125
163
 
126
164
  ```tsx
@@ -186,7 +224,7 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
186
224
 
187
225
  `ActionBar` keeps the actions of a long page on a glass bar stuck to the bottom of the viewport, with an optional short `status` (such as when the work was last saved): a string, or a node such as a `Badge` and then a short line. Put it last in the content it acts on: it stays stuck while that content scrolls by and then rests in its own place at the end. `stickyBelow="md"` sticks only on a phone and leaves the bar in page flow from `md` up. The bar clears `env(safe-area-inset-bottom)` (set `viewport-fit=cover` in the page's viewport meta for the inset to apply). Its children are the actions: on a phone they share one row under the status. The status is a polite live region (`role="status"`, kept in the page even while empty), so a new status such as "Draft saved at 5:00 PM" is announced; do not announce the same save a second time with your own notice. The status is a `div` whose children sit on one row a `space-2` gap apart and wrap under one another when the row is full, so a line that does not fit beside its badge drops under it on a phone; put a phrase that mixes text and elements in one `span` so it wraps as a sentence. `undefined`, `null` or `false` leaves the status empty. An open `Select`, `ActionMenu` or `Tooltip` paints over the bar, including inside a glass `Card` or `Accordion`.
188
226
 
189
- `ActionBar` `summary` (`{ label, value, detail?, tone? }`, type `ActionBarSummary`) puts a labelled figure first in the same polite status region, before any `status`: the label and the detail in muted label-size words, the value between them in the `heading` variant (24 below `md`) at tabular figures, all on one baseline and wrapping as one phrase, such as "Counted **$33,790.00** short $4,000.00". `tone` (`'default' | 'danger' | 'success'`, such as danger while short or over and success once it matches) colors the value only, so say why in the detail. Format the value yourself; each new figure is announced politely. For a guided form, the actions are a secondary Cancel and one primary that stays `disabled` until the form is valid. A blank `label`, `value` or `detail`, or an unknown `tone`, throws a `RangeError`.
227
+ `ActionBar` `summary` (`{ label, value, detail?, tone? }`, type `ActionBarSummary`) puts a labelled figure first in the same polite status region, before any `status`: the label and the detail in muted label-size words, the value between them in the `heading` variant (24 below `md`) at tabular figures, all on one baseline and wrapping as one phrase, such as "Counted **$33,790.00** short $4,000.00". `tone` (`'default' | 'danger' | 'success'`, such as danger while short or over and success once it matches) colors the value only, so say why in the detail. Format the value yourself; each new figure is announced politely. For a guided form, the actions are a secondary Cancel and one primary that stays `disabled` until the form is valid. An empty `value` (`''` or only spaces) means there is no figure yet, such as before anything is counted: the summary line is left out and the live region stays, so the first figure is announced when it arrives. Leaving `summary` out is the same; format the value before you pass it, since an empty one hides its detail and tone too. A blank `label` or `detail`, a `value` that is not a string, or an unknown `tone` throws a `RangeError`.
190
228
 
191
229
  ```tsx
192
230
  <ActionBar
@@ -237,6 +275,8 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
237
275
 
238
276
  `ButtonGroup` `appearance="track"` (an `attached` group only; default `"default"`, `buttonGroupAppearances`) sets the joined buttons as words in the text color on one quiet grey track (the muted fill at 16 %, the sm radius) with the `border` token's hairline between them, whatever their variant, such as Previous, This week and Next above a period. It is for equal-weight navigation: every variant's meaning goes, so keep a primary or destructive action out of it. A pressed button keeps its ring, a focused one its outline and a disabled one fades (`disabledAppearance="neutral"` too); forced colors outline the track, keep every border and draw a pressed button's in Highlight. An unknown value, or `track` without `attached`, throws a `RangeError`.
239
277
 
278
+ `ButtonGroup` `fullWidthBelow="md"` (an `attached` group only; the layout `Breakpoint`) makes the joined control fill its column below that breakpoint, its buttons sharing the width equally (each may shrink, so keep their words short), such as earlier, a period and later across a phone; from `md` up it sizes to its buttons as before. On a group that is not attached it throws (that row already stacks full width on a phone), as does an unknown breakpoint.
279
+
240
280
  `Badge` is a non-interactive chicklet with a `tone` (`neutral`, `accent`, `success`, `danger`, `warning`) and a `size` (`md`, or `sm` for a table cell). A badge inside a filled `Button` (every variant but `ghost`) sits on `--sw-color-surface` with its words in the text color and its tone on its border, so it reads at 4.5:1 in every palette whatever the button's fill.
241
281
 
242
282
  `ActionMenu` opens independent commands from a labelled button. Provide localized command labels and callbacks; use `disabled` for unavailable commands and `destructive` for a dangerous command's presentation. Escape and choosing a command both return focus to the trigger (before the command runs, so a `Dialog` it opens hands focus back to the trigger on close), and outside interaction dismisses the menu. The open menu sits on the popover layer a `space-1` gap below its trigger (above it when it only fits there), at least `space-2` from every viewport edge (a classic scrollbar is not part of the viewport), and aligned to the trigger's inline start (its right edge in right-to-left), or to its other edge when the trigger ends a row and the menu would not fit from its start. `align="end"` (default `"start"`) lines the menu up with the trigger's inline end even where the start would fit, for a trigger that ends a card or a list row, so the menu stays over that card instead of hanging past it; it takes the start when the end would leave the screen. The menu is as wide as its longest command up to the viewport less both insets, and a longer command wraps. The menu follows its trigger through scroll, window resize, and a change in the menu's or the trigger's size. The menu is in the DOM only while it is open. The trigger and its commands use the compact xs control height for a fine pointer; on a coarse pointer the trigger is at least 44 × 44 px (the md control height, as on `CalendarButton`) and every command is at least 44 px tall. An item's `detail` is its current value at the command's end, such as "English" on a Language command that opens a choice: muted, before a chevron that points to the inline end (mirrored right to left), and read as part of the command's name ("Language English"); `detailLang` marks its language. It is meant for a short value; a long one wraps between words, never inside one. The command runs like any other and opens no submenu, so a command that opens a dialog or a picker says so in its label. A blank `detail`, or a blank `detailLang` or one without a `detail`, throws a `RangeError`. `separatorBefore` draws a hairline (`role="separator"`) before a command, setting it apart, such as Sign out; the arrow keys skip it, and it is never drawn before the first command.
@@ -266,7 +306,7 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
266
306
 
267
307
  `Dialog` is a modal `<dialog>` controlled by `open`. Escape, a backdrop press, a platform close request, and a `<form method="dialog">` submit (or a submitter with `formmethod="dialog"`) are each prevented and call `onClose`; none of them closes the dialog itself. A consumer `onKeyDown`, `onCancel`, `onSubmit` or `onPointerDown` that prevents the event vetoes that request. Escape in a search field that holds text clears the field first, and a descendant's own `cancel` (a dismissed file picker) is not a close request. Set `open` to false to close it, or keep it true (for example while a form is saving) and it stays shown. `onClose` is not called when `open` turns false, and it is required: a dialog that must not be dismissed passes a callback that keeps `open` true. The `title` is an `h3` in the title style by default; `titleLevel={2}` renders it as an `h2` in the same style, for a dialog whose own section labels are `h3` headings, so they read as parts of the dialog and not as its peers. The default stays `3` so existing dialogs keep their outline; any value other than `2` renders an `h3`. `sheetBelow="md"` makes the dialog a bottom sheet below `md`: docked to the bottom edge at the full viewport width, top corners rounded and bottom corners square, a `space-8` strip of backdrop above it, and bottom padding that clears `env(safe-area-inset-bottom)` (with `viewport-fit=cover`); it slides up on open unless reduced motion is asked for, and has no grabber, since there is no drag. From `md` up it is the centered dialog at its `size`. `closeLabel` (such as "Close") adds an icon-only ghost close button at the end of the title row, named by the label, which calls `onClose` like Escape; the title still names the dialog. The button is the dialog's first control, so the browser focuses it on open; give a field `autoFocus` to start there instead. Without `closeLabel` the dialog has no close button and its markup is unchanged.
268
308
 
269
- A sheet over a page takes a fuller frame. `icon` is a consumer glyph or tile before the title (hidden from assistive technology) and `description` a muted line under the title that also describes the dialog (unless you pass `aria-describedby`). `footer` holds the actions, pinned to the dialog's bottom edge under a hairline while the content scrolls, and `summary` (ActionBar's `{ label, value, detail?, tone? }`) puts a labelled figure at the footer's start in a polite live region. `size="xl"` (65rem) holds a six-column count grid with its labels and total. `role="alertdialog"` is for a warning that interrupts, such as an idle sign-out: give its safe action `autoFocus` (the browser otherwise focuses the first footer button), and point `aria-describedby` at the warning in its body when that is not the `description`. `appearance="banded"` (default `"glass"`) draws the dialog on the solid surface with the medium elevation instead of the frosted panel: its title row is a full-width band over a hairline (a lone title too), the footer the band it already is, and the close button a circle on the quiet muted fill in the text color, keeping its touch target; forced colors outline the dialog and keep the hairlines. An unknown `size`, `role` or `appearance`, a blank `description` or an invalid `summary` throws a `RangeError`.
309
+ A sheet over a page takes a fuller frame. `icon` is a consumer glyph or tile before the title (hidden from assistive technology) and `description` a muted line under the title that also describes the dialog (unless you pass `aria-describedby`). `footer` holds the actions, pinned to the dialog's bottom edge under a hairline while the content scrolls, and `summary` (ActionBar's `{ label, value, detail?, tone? }`) puts a labelled figure at the footer's start in a polite live region. `size="xl"` (65rem) holds a six-column count grid with its labels and total. `role="alertdialog"` is for a warning that interrupts, such as an idle sign-out: give its safe action `autoFocus` (the browser otherwise focuses the first footer button), and point `aria-describedby` at the warning in its body when that is not the `description`. `appearance="banded"` (default `"glass"`) draws the dialog on the solid surface with the medium elevation instead of the frosted panel: its title row is a full-width band over a hairline (a lone title too), the footer the band it already is, and the close button a circle on the quiet muted fill in the text color, keeping its touch target; forced colors outline the dialog and keep the hairlines. `status` is one short line in the footer after the summary and before the actions, in the same polite live region: muted caption words, or a `Badge` and a short line in one `span` (a status alone makes a footer; blank words or `false` leave it out). `canvas="grouped"` (default `"default"`) fills the dialog with the grouped canvas grey that `ThemeProvider canvas="grouped"` paints, solid with no blur, so `Card`s inside stand apart; a banded title row and the footer keep the surface. `sheetGrabber` (with `sheetBelow`, which it needs) draws a grabber, a short rounded bar centered at the top of the bottom sheet, hidden from assistive technology; it stays at the top while the content scrolls, does not drag, and is not drawn from the breakpoint up. An unknown `size`, `role`, `appearance` or `canvas`, a blank `description`, an invalid `summary`, or `sheetGrabber` without `sheetBelow` throws a `RangeError`.
270
310
 
271
311
  ```tsx
272
312
  <Dialog
@@ -291,6 +331,26 @@ A sheet over a page takes a fuller frame. `icon` is a consumer glyph or tile bef
291
331
  </Dialog>
292
332
  ```
293
333
 
334
+ ```tsx
335
+ <Dialog
336
+ open={open}
337
+ onClose={close}
338
+ sheetBelow="md"
339
+ sheetGrabber
340
+ canvas="grouped"
341
+ title="Tally the roosts"
342
+ summary={{
343
+ label: 'Bats',
344
+ value: counted ? String(counted) : '',
345
+ detail: 'in all roosts',
346
+ }}
347
+ status="Nothing counted yet"
348
+ footer={footerActions}
349
+ >
350
+ <Card padding={4}>…</Card>
351
+ </Dialog>
352
+ ```
353
+
294
354
  `Toast` is an auto-dismiss confirmation on the popover layer, controlled by `open` and `onOpenChange`; it does not trap focus and closes after `timeoutMs` (default 800, or 6000 for a `warning` or `danger` toast, which must be read). `anchor` and `target` send it from a press to a destination. `tone` (`neutral` by default, `success`, `warning`, `danger`) tints its border and an optional consumer `icon` (a Lucide glyph, hidden from assistive technology); the message keeps the text color, so its words carry the meaning. A `danger` toast is announced as `role="alert"`, the others as `role="status"`, so do not wrap the message in another live region. A toast still dismisses itself: an error the person must act on belongs in the page. A toast is as wide as its message, up to the page's gutter (spacing step 4) on each side, so on a phone a long message wraps inside the gutter instead of running to the screen's edges.
295
355
 
296
356
  A toast can also be a corner card that confirms a save: `placement="top-end"` puts it `space-4` from the viewport's top and end corner, `title` is a bold first line with the children as a muted detail under it (pass plain text, not a `Text`), `action` (`{ label, onPress }`) is one ghost small Button at the end that runs `onPress` and then closes the toast, and `showRemaining` adds a thin line that runs down with the time left. A toast with an action or the line takes the pointer, and its time (and the line) is held while the pointer is over it or focus is inside it; each opening starts with the whole time. Pass the time you want, such as `timeoutMs={10000}`, and close it yourself when it no longer applies (for example when the next sale starts). It is never modal and never takes focus; when it closes with focus inside (after its action), focus goes back to where it came from. Keyboard users reach the action in the Toast's place in the DOM, so render it right after the control that saves. A travelling toast ignores `placement` and `showRemaining`.
@@ -313,6 +373,19 @@ A toast can also be a corner card that confirms a save: `placement="top-end"` pu
313
373
 
314
374
  `Switch` is a controlled on/off input. It keeps native checkbox keyboard behavior and exposes switch semantics. Provide the current `checked` value and update it in `onCheckedChange`.
315
375
 
376
+ `Switch` `layout="trailing"` (`switchLayouts`, default `"leading"`) puts the label and description first and the control at the end of a row as wide as its column, lined up with the label's first line, as a settings list draws it; the reading order follows. `checkedTone="success"` (`switchCheckedTones`, default `"accent"`) fills the track with the success colour while on, for a setting whose on state is the healthy one (3:1 or more on the surface in every palette; `Highlight` in forced colors); the thumb's place says the state either way. An unknown layout or tone throws a `RangeError`.
377
+
378
+ ```tsx
379
+ <Switch
380
+ label="Number the observers"
381
+ description="Names stay out of the file"
382
+ layout="trailing"
383
+ checkedTone="success"
384
+ checked={numbered}
385
+ onCheckedChange={setNumbered}
386
+ />
387
+ ```
388
+
316
389
  `Checkbox` is a controlled native checkbox for one independent form or confirmation choice. `checked` and `onCheckedChange` own its state. The consumer supplies `error` after validation to describe and style an invalid choice (associated by `aria-describedby` with `aria-invalid` and announced from a polite live region, as `Field`'s error is, never an alert); `required` retains native form semantics.
317
390
 
318
391
  `RadioGroup` is a controlled fieldset of native radio choices. Supply a unique nonempty `value` for each option, one selected `value` or an empty value for no selection, a `legend`, and `onChange`. Long labels wrap in a vertical group. The consumer provides localized option labels and validation `error`; `required` keeps native form semantics. An option's optional `icon` (a `ReactNode`, such as a Lucide glyph) sits between the radio and the label, in the text color and the option's gap; it is `aria-hidden`, so the radio's accessible name stays the `label` text, and a press on it chooses the option as the label does.
@@ -355,6 +428,23 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
355
428
 
356
429
  `SegmentedControl` is one exclusive choice shown as a track of segments (`role="radiogroup"`), named by `aria-label` or `aria-labelledby`, with a controlled `value` and `onChange`; `variant="filled"` gives every segment the same width and fills the selection with the accent, and `disabled` keeps the choice visible but inert. `error` puts a message under the track (`sw-field-error`), links it to the group by `aria-describedby` and sets `aria-invalid` and a danger outline on the group (`Mark` in forced colors); as `Field`'s it is announced politely from a live region that is always there, never as an alert, and an empty string is no error. `required` sets `aria-required` on the group; since the control has no label of its own, the element that labels it shows the mark. The track sits in a `.sw-segmented-field` wrapper that holds the message and takes the track's place in the layout, so a `ref` still reaches the `radiogroup`. `error` is for a control in a `Stack` or block layout, where the track keeps its width and the message wraps under it. While it shows a message the field is at least 24ch wide, capped at its container, so a short compact track does not squeeze the message into a column. In an `Inline` the field therefore grows and the rest of the row moves, while the track keeps its labels' width. An item's `icon` (such as a small `GlyphTile`) sits before its label, hidden from assistive technology, so the label names the segment; when any item has one, a compact track takes the `md` control height and label type on a subtle fill, with the selected item raised, and a filled track keeps its look and only gains the marks.
357
430
 
431
+ `SegmentedControl` `fullWidthBelow="md"` makes the control fill its column below that breakpoint, its segments sharing the width equally whatever the variant (a raised track too); from `md` up it keeps its own width. `size="sm"` (`segmentedSizes`, default `"md"`) pads every segment a step less and keeps each label on one line, ending in an ellipsis past its share while its name keeps the whole label, so three or four segments with counts fit a phone's card (a segment with a mark keeps its padding, and the label truncates on any variant). The ellipsis is a last resort that sighted people cannot expand: keep `sm` labels short enough to fit at the widths you expect, at 200 % zoom too, and never let the hidden part carry meaning. An unknown breakpoint or size throws a `RangeError`.
432
+
433
+ ```tsx
434
+ <SegmentedControl
435
+ aria-label="Field log"
436
+ variant="raised"
437
+ size="sm"
438
+ fullWidthBelow="md"
439
+ items={[
440
+ { id: 'changes', label: 'Changes 57' },
441
+ { id: 'people', label: 'People 12' },
442
+ ]}
443
+ value={log}
444
+ onChange={setLog}
445
+ />
446
+ ```
447
+
358
448
  `variant="raised"` (opt-in; default `"compact"`) is a grey track (the quiet muted fill, no border, corners concentric with the segments' small radius) as wide as its segments even in a `Stack`; every segment is the `md` control height in label type with its words in the text color at medium weight, and the selected one is the surface raised by the small elevation at label weight; in the dark scheme, where a shadow does not show, it draws a hairline ring in the muted color instead (3:1 or more on the track in every dark palette). Item `icon`s keep that look, and `error` and `disabled` behave as on the other tracks. It is the same radiogroup with the same keys; forced colors outline the track and the selected segment.
359
449
 
360
450
  ```tsx
@@ -378,7 +468,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
378
468
 
379
469
  `appearance="words"` (opt-in; default `"default"`, `navAppearances`) draws every destination, a link or a `Button` of any variant, as plain words: no fill, border or underline, label type, the others in the muted color at medium weight and the current one (`aria-current`, not `"false"`) in the text color at label weight. A Button keeps its size, padding, touch target and focus ring; hover lifts a destination to the text color where the pointer can hover, never a disabled one. The current mark is color and weight, so each destination's words must be visible: a glyph-only destination has no weight to change (keep `default` for a glyph rail). Forced colors draw the current one's border in Highlight and underline its words. An unknown value throws a `RangeError`.
380
470
 
381
- `NavCount` puts a count after a destination's words, such as the tasks waiting in an area: place it inside the link or `Button`, after its words (`<a href="/tasks">Tasks<NavCount count={8} /></a>`). It is the pill `SectionNav`'s item `count` draws (the danger color with its on-color figures, an edge in forced colors), a space-2 after the words in a link or a Button, the link keeping its siblings' height, and it is read as part of the destination's name ("Tasks 8"). `count` is a whole number; 0 shows nothing. `countLabel` says what it means in your words ("8 tasks to do"): it is read in place of the bare number ("Tasks 8 tasks to do") and the pill is hidden from assistive technology. A count that is not a whole number from 0, or a blank `countLabel`, throws a `RangeError`.
471
+ `NavCount` puts a count after a destination's words, such as the tasks waiting in an area: place it inside the link or `Button`, after its words (`<a href="/tasks">Tasks<NavCount count={8} /></a>`). It is the pill `SectionNav`'s item `count` draws (the danger color with its on-color figures, an edge in forced colors), a space-2 after the words in a link or a Button, the link keeping its siblings' height, and it is read as part of the destination's name ("Tasks 8"). `count` is a whole number; 0 shows nothing. `countLabel` says what it means in your words ("8 tasks to do"): it is read in place of the bare number ("Tasks 8 tasks to do") and the pill is hidden from assistive technology. `tone="accent"` (default `"danger"`, `navCountTones`) draws the pill in the accent color with its on-color figures, for a count that is news rather than work waiting, such as what was added since the person last looked; its size, spacing and name are the same. Give an accent count a `countLabel` ("3 new") so its words, not its color, say it is news, and place either tone on a neutral or ghost destination, never a filled one of the same color. A count that is not a whole number from 0, a blank `countLabel` or an unknown `tone` throws a `RangeError`.
382
472
 
383
473
  ```tsx
384
474
  <Nav appearance="words" aria-label="Areas">
@@ -389,6 +479,10 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
389
479
  Tasks
390
480
  <NavCount count={8} countLabel="8 tasks to do" />
391
481
  </Box>
482
+ <Box as="a" href="/field-notes">
483
+ Field notes
484
+ <NavCount count={3} countLabel="3 new" tone="accent" />
485
+ </Box>
392
486
  </Nav>
393
487
  ```
394
488
 
@@ -462,7 +556,18 @@ A ledger read across as well as down takes `Table` `columnRules` (a hairline at
462
556
  </Table>
463
557
  ```
464
558
 
465
- `BarChart` `formatValue` writes the axis's values and each value without its own `valueLabel`, such as a currency. `diverging` puts zero in the middle of each track behind a hairline: a positive bar grows toward the inline end and a negative one toward the start, on one scale, and the axis reads minus the maximum, zero and the maximum. Without it every bar grows from the start. An item's `tone` (`BarChartTone`: `'accent'`, `'success'`, `'warning'` or `'danger'`, listed in the exported `barChartTones`) colors its bar by what it means in place of its sign's default (the accent, or danger for a negative value), such as a diverging chart's overage in `'warning'` and shortage in `'danger'`; a toned bar keeps its sign's place, every tone keeps 3:1 against the track, and an unknown tone throws a `RangeError`. Forced colors draw every bar in the system text color, so the value and its side of zero carry the meaning there.
559
+ `BarChart` `formatValue` writes the axis's values and each value without its own `valueLabel`, such as a currency. `diverging` puts zero in the middle of each track behind a hairline: a positive bar grows toward the inline end and a negative one toward the start, on one scale, and the axis reads minus the maximum, zero and the maximum. Without it every bar grows from the start. An item's `tone` (`BarChartTone`: `'accent'`, `'success'`, `'warning'`, `'danger'` or `'neutral'`, listed in the exported `barChartTones`) colors its bar by what it means in place of its sign's default (the accent, or danger for a negative value), such as a diverging chart's overage in `'warning'` and shortage in `'danger'`, or the usual days in `'neutral'` (the muted grey) beside the busiest ones in the accent; a toned bar keeps its sign's place, every tone keeps 3:1 against the track, and an unknown tone throws a `RangeError`. Forced colors draw every bar in the system text color, so the value and its side of zero carry the meaning there. `appearance="bare"` (`barChartAppearances`, default `"track"`) draws the bars alone, with no track and no value axis, for bars whose values are all written beside them; the track's room is kept, so labels and values line up as before, a diverging bare chart keeps its zero hairline, and every tone keeps 3:1 against the surface it sits on; in forced colors the track's edge is the canvas color. A bar's tone is the charts' stroke color of that tone (`chart-tones.ts`); a `ChartLegend` `bar` swatch takes the lighter fill of a tone, so key a neutral or warning bar with a `marker` or `line` swatch. An unknown appearance throws a `RangeError`.
560
+
561
+ ```tsx
562
+ <BarChart
563
+ appearance="bare"
564
+ aria-label="Owls heard by night"
565
+ items={[
566
+ { label: 'Thu', value: 3, tone: 'neutral' },
567
+ { label: 'Fri', value: 9 },
568
+ ]}
569
+ />
570
+ ```
466
571
 
467
572
  `DataBar` is a horizontal range bar for a table cell or a list row: a bar from `from` to `to` (whichever is larger is its far end) on a track whose domain runs from `min` to `max`, such as where a statement's line leaves the running total. It fills its container's width, a spacing step 4 tall with no track fill, and its ends follow the writing direction. It is decorative (`aria-hidden`): the figure is the cell's own text, so keep the value beside it, and a cell holding both is named by the figure alone. `tone` (`DataBarTone`, listed in `dataBarTones`; default `'neutral'`) says what the bar is: `'neutral'` for where it started, `'success'` for what came in, `'muted'` for what went out (never danger: an outflow is not an error), `'strong'` for a subtotal, `'accent'` for what was counted or recorded, and `'danger'` for a shortage, never for cash leaving (a light red, the danger colour at 40 %). Each is a semantic colour mixed into the surface, 1.5:1 or more on it, the three greys 1.5:1 or more apart, the shortage's red apart from the grey of what went out, in every palette and scheme. A domain from below zero to above it (`min < 0 < max`) draws a hairline at zero, so a column of differences diverges around it (`from={0} to={value}`), and the bar's end at zero is square; a short bar in `'danger'` and an over bar in `'success'` differ by hue alone, so the bar's side of zero and the signed figure carry the meaning. Values past the domain are clamped to it; non-finite numbers, a `min` not under `max`, or an unknown tone throw a `RangeError`. Forced colors draw the bar and the zero line in the system text color.
468
573
 
@@ -539,6 +644,31 @@ A bar in its own column reads as empty cells to assistive technology, which is f
539
644
  />
540
645
  ```
541
646
 
647
+ `DocumentViewer` draws a PDF from its bytes (`data`, an `ArrayBuffer` or `Uint8Array`, never a URL) with PDF.js, one page at a time, fitted to the width it is given, on a quiet grey stage at the large radius. Under the page, "Page n of m" (`labels.page(page, pageCount)`) is a polite live region between 48-point round previous and next steps (`labels.previous`, `labels.next`), which look off at either end but stay Tab stops; with one page only the words show. `page` and `onPageChange` hold the page in the consumer (left out, the viewer keeps its own; a page out of range shows the nearest). Two fingers zoom the page up to four times as wide, keeping the spot of the page between them under the fingers (the stage scrolls across and the boxes around it, the window at last, scroll down; the stage only pans, so the screen does not zoom instead), and on the focused page `+`, `-` and `0` zoom in, out and back to fit width about the middle of what shows; a zoomed page scrolls sideways inside the stage and is drawn again at its size once the zoom rests. The page's text lies over the drawing, transparent (PDF.js's text layer), so assistive technology reads it and the person can select it. While the PDF opens it shows `labels.loading`; when it cannot be opened or a page cannot be drawn, `labels.failed`, an optional `labels.failedDetail` and your `failedAction` (such as Try again and Download PDF) in an alert, and it calls `onError`; when only one page cannot be drawn, the steps stay, so another page can be read. `onLoad` receives the page count. New `data` opens again at the first page, so Try again is new bytes or a new `key`; keep `data` the same object between renders (in state or a memo), since bytes made afresh on each render reopen the PDF every time. The viewer is a region landmark named by `label`; its stage, once open, is a focusable group named by the page words. It has no download or print of its own and opens nothing from the network: no font, character map, colour profile or WebAssembly file (PDF.js uses system fonts and skips what it cannot decode), no XFA, no streaming or range requests, and no code evaluation (`isEvalSupported: false`); it draws from a copy, so your bytes stay usable for a download. PDF.js (`pdfjs-dist`, its legacy build, for the browsers stores run) is loaded the first time a viewer opens a document, so a page without one never downloads it. Missing words, a blank `workerSrc` or a `labels.page` that is not a function throw a `RangeError`.
648
+
649
+ PDF.js parses in a worker. Scalewing ships the worker entry for its own pinned PDF.js version as `@scalewing/react/document-viewer-worker`; bundle it as a module worker and pass its URL as `workerSrc`. With Vite:
650
+
651
+ ```tsx
652
+ import { DocumentViewer } from '@scalewing/react';
653
+ import workerSrc from '@scalewing/react/document-viewer-worker?worker&url';
654
+
655
+ <DocumentViewer
656
+ data={bytes}
657
+ workerSrc={workerSrc}
658
+ label={guide.title}
659
+ labels={{
660
+ previous: t('reader.previousPage'),
661
+ next: t('reader.nextPage'),
662
+ page: (page, count) => t('reader.pageOf', { page, count }),
663
+ loading: t('reader.opening'),
664
+ failed: t('reader.failed'),
665
+ }}
666
+ failedAction={<Button onPress={retry}>{t('reader.tryAgain')}</Button>}
667
+ />;
668
+ ```
669
+
670
+ The consumer needs no PDF.js dependency of its own. With another bundler, build `@scalewing/react/document-viewer-worker` as a module worker (`new Worker(url, { type: 'module' })`) and pass the built file's URL. `pdfjs-dist` has an optional native canvas for Node (`@napi-rs/canvas`, 28 to 35 MB per platform) that a browser never loads; a pnpm consumer can skip it with `ignoredOptionalDependencies`. `pdfjs-dist` asks for Node 22.13 or later.
671
+
542
672
  `Spinner` shows indeterminate loading in small, medium, or large sizes. Supply localized `label` for the one announced status in a loading region. Use `decorative` on additional indicators beside that status so screen readers do not hear the same message repeatedly. Reduced motion leaves a static accented ring.
543
673
 
544
674
  `GlyphTile` is a small rounded square in a categorical colour with the consumer's glyph in white: the mark of a kind beside its name, such as a service in a list. `hue` is one of the `@scalewing/tokens` categorical colours (`green`, `blue`, `orange`, `purple`, `teal`, `rose`, or `neutral`); which kind takes which hue is yours. `size` is `sm` (24), `md` (28, the default) or `lg` (32); the glyph is drawn at spacing step 4 in white, which keeps 3:1 on every fill; pass the SVG (a Lucide component) as the tile's direct child, since only a direct-child SVG is sized. It is always `aria-hidden`, so put the kind's name in visible text beside it. An unknown `hue` or `size` throws a `RangeError`.
@@ -614,6 +744,48 @@ A bar in its own column reads as empty cells to assistive technology, which is f
614
744
  </Inline>
615
745
  ```
616
746
 
747
+ `Article` is a page that reads like a post, on a card (`variant`, as `Card`'s) no wider than the reading width (`--sw-reading-max`, 46rem): an optional `cover` photo (`{ src, alt }`, `alt` required, `''` when it only decorates) (or `null`) edge to edge across the top at 3:1 (2:1 below `md`) with the card's top corners; then, a space-6 from the top and bottom and a space-8 from the sides (space-5 all round below `md`), the `eyebrow` (muted caption type at the label's weight) with your `actions` at its row's end (such as a language switch; under the eyebrow, at its start, when the row is too narrow), the `title` in heading type (`titleAs`: `h1` by default, on the article's own page; `h2` or `h3` under other headings), the muted `meta` line (such as "Manager 1 · Oct 16 · 4 min read"), a hairline, and the body (`children`, your own content, such as paragraphs or Markdown your app draws). It is an `article` landmark named by its title, and its ref is that element; the eyebrow and meta are `div`s, so any content fits. The card sits at the start of a wider column. Empty parts draw nothing. A blank `title`, an unknown `titleAs` or `variant`, or a `cover` with a blank `src` or no `alt` throws a `RangeError`.
748
+
749
+ ```tsx
750
+ <Article
751
+ cover={{ src: cover.url, alt: cover.alt }}
752
+ eyebrow="Field guide · Wetlands › Herons"
753
+ title="How to count herons at the north hide"
754
+ meta="Ranger 1 · Oct 16 · 4 min read"
755
+ actions={
756
+ <SegmentedControl
757
+ aria-label="Language"
758
+ variant="raised"
759
+ items={languages}
760
+ value={language}
761
+ onChange={(id) => setLanguage(id === 'es' ? 'es' : 'en')}
762
+ />
763
+ }
764
+ variant="outlined"
765
+ >
766
+ <Stack gap={4}>{paragraphs}</Stack>
767
+ </Article>
768
+ ```
769
+
770
+ `MarkdownView` draws Markdown someone wrote or pasted (`source`) in Scalewing's type, as React elements and never as HTML from a string: paragraphs, headings (`#` drawn as an `h2`, since the page has its own title, `##` an `h3`, `###` and deeper an `h4`, in the title, body and label sizes; a `#` followed straight by a `###` skips a level, as written), bullet and numbered lists, bold, italic, links, block quotes as a highlighted box (the accent's tint with an accent bar; a link or photo caption in it is the text colour), line breaks, thematic breaks (a `Separator`) and photos. A link opens only `http`, `https` or `mailto` addresses, read as the browser would (`new URL`), in a new tab with `rel="noopener noreferrer"`, and carries your `newTabLabel` (required, such as "(opens in a new tab)") as hidden words; reference links and autolinks follow the same rule, and any other scheme (such as `javascript:` or `data:`), a relative or protocol-relative address shows as the link's plain words. Links are the accent, underlined; in the harvest palette's dark scheme the accent is 4.24:1 on the surface, a palette gap the tokens tests record. The only image is `![caption](photo:<id>)`: `resolveImage(id)` returns its `{ src, alt? }` or `null`; it is drawn as a figure at the page's width with the caption under it, outside any paragraph, when its `src` is relative, `https:`, `http:`, `blob:` or a `data:` PNG, JPEG, GIF, WebP, AVIF or SVG and it has words (its `alt`, else the caption). Keep `resolveImage` stable (`useCallback`): a new function reads the source again. Raw HTML (a tag's words stay), scripts, tables, other images and unresolved or wordless photos are left out; code is shown as plain text without highlighting. Quotes, lists and marks nested past 16 levels are shown as their words (a block reaching the parser's 128th level, where it stops reading, is reported once as it may be lost), and the parser's work stays linear, so a realistic 50,000-character page reads in about 7 ms and the slowest crafted one in about a tenth of a second (measured in the request). Each heading gets an id from its words, after `idPrefix`: give every view on a page its own (such as `"page-"`; `''`, the default, is none), or a heading such as "Content" takes the page's `content` id; repeats get `-2`, `-3`. `inspectMarkdown(source, { resolveImage, idPrefix })` gives the same `headings` (`id`, `level`, `text`) for an "On this page" `Nav`, the first 100 `omissions` (`kind`: `html`, `table`, `image`, `link`, `code` or `limit`, and up to 80 characters of `source`) and `omissionCounts` for every kind, for an editor to say what will not be shown. The parser is `markdown-it` (CommonMark, with GFM tables only so they are recognised and left out); its HTML renderer is never used. A `source` that is not text, an `idPrefix` other than `''` or a letter followed by letters, digits, `-` or `_`, or a `resolveImage` that is not a function throws a `RangeError` in both; a blank `newTabLabel` throws in the view.
771
+
772
+ ```tsx
773
+ // One stable resolver for both: a new function reads the source again.
774
+ const resolveImage = useCallback((id: string) => photos[id] ?? null, [photos]);
775
+
776
+ const { headings, omissions } = inspectMarkdown(page.body, {
777
+ resolveImage,
778
+ idPrefix: 'page-',
779
+ });
780
+
781
+ <MarkdownView
782
+ source={page.body}
783
+ resolveImage={resolveImage}
784
+ idPrefix="page-"
785
+ newTabLabel="(opens in a new tab)"
786
+ />;
787
+ ```
788
+
617
789
  `Separator` divides sections with a token-colored line. It is horizontal by default; use `orientation="vertical"` in a flex row. Set `decorative` when the line only supports layout so assistive technology ignores it.
618
790
 
619
791
  `DescriptionList` pairs terms with what each says, one `DescriptionItem` per row: a `dl` whose items each group a `dt` (`term`) with its `dd` (`children`). From `md` up the term sits in a start column as wide as the widest term, up to 40% of the list, and the detail beside it; every row aligns to its top, so a term and its detail's first line start level, and a hairline divides the rows, with no space above the first row or below the last. Below `md` each term sits over its detail. The list sets only the layout: pass your own `Text` for the term and the detail. Each child of a term or a detail is its own line with its own line height (a column), as wide as its content unless it sets its own width (a `Progress` fills the detail; a `Field` keeps its input's width): wrap inline content, such as words and a link on one line, in one element such as an `Inline`. Long unbreakable words wrap inside their column. A `dl` has no ARIA role, so name a list with a visible heading, and `aria-labelledby` on the `section` around it, rather than an `aria-label` on the list.
@@ -660,7 +832,35 @@ A bar in its own column reads as empty cells to assistive technology, which is f
660
832
  </Card>
661
833
  ```
662
834
 
663
- `ListGroup` is rows in one list with a hairline between each pair, the web twin of react-native's: a `ul` (its list role kept) named by `aria-label` or `aria-labelledby`. `variant="panel"` (the default) is one bordered panel; `plain` draws no panel and a hairline above the first row too, for rows inside a `Card`. Each `ListRow` has a `title` (weight 600, or the body weight with `titleAppearance="plain"`; `tone="danger"` for a row that ends something), an optional muted `detail` line, an optional `value` at its end (weight 600, tabular figures; `valueTone` `success`, `danger`, `muted` or `accent`; `valueDetail` a muted line under it) and an optional `leading` mark such as a status dot, hidden from assistive technology, so the words say what it means. With `onPress` the whole row is a button, and with `href` a link (`onPress` then takes a plain press for a client router); either adds a chevron, a quiet fill under the pointer and 44 px on a coarse pointer, and the row is named by its title and value and described by its detail lines (`aria-label` replaces the name). `selected` makes a pressable row a choice: a check while true, `aria-pressed` on a button or `aria-current` on a link (on a row with neither `onPress` nor `href` it throws, since nothing could read the state). `accessory="none"` hides the chevron of a row that acts in place; `disabled` blocks presses. A row with neither `onPress` nor `href` only states something. `maxHeight={{ rows }}` bounds the list to that many rows of a title and one detail line, the rest scrolling inside its own named region, which shades an edge with more rows past it; a bounded list must be named. `footer` stays pinned under the rows, outside the scroll, such as a `ListRow` with the count and the total (a row there is not a list item). An empty title or value, an unknown variant or tone, a `valueDetail` without a value, or a bad row count throws a `RangeError`.
835
+ `ListGroup` is rows in one list with a hairline between each pair, the web twin of react-native's: a `ul` (its list role kept) named by `aria-label` or `aria-labelledby`. `variant="panel"` (the default) is one bordered panel; `plain` draws no panel and a hairline above the first row too, for rows inside a `Card`. Each `ListRow` has a `title` (weight 600, or the body weight with `titleAppearance="plain"`; `tone="danger"` for a row that ends something), an optional muted `detail` line, an optional `value` at its end (weight 600, tabular figures; `valueTone` `success`, `danger`, `muted`, `accent` or `warning`, the warning colour's ink moved halfway to the text colour so it keeps 4.5:1 on the surface in every palette; `valueBadge` draws the value as a `Badge` in that tone instead, such as a status, still read in the row's name, never with `valueTone`; `valueDetail` a muted line under it) and an optional `leading` mark such as a status dot, hidden from assistive technology, so the words say what it means. With `onPress` the whole row is a button, and with `href` a link (`onPress` then takes a plain press for a client router); either adds a chevron, a quiet fill under the pointer and 44 px on a coarse pointer, and the row is named by its title and value and described by its detail lines (`aria-label` replaces the name). `selected` makes a pressable row a choice: a check while true, `aria-pressed` on a button or `aria-current` on a link (on a row with neither `onPress` nor `href` it throws, since nothing could read the state). `accessory="none"` hides the chevron of a row that acts in place; `disabled` blocks presses. A row with neither `onPress` nor `href` only states something. `maxHeight={{ rows }}` bounds the list to that many rows of a title and one detail line, the rest scrolling inside its own named region, which shades an edge with more rows past it; a bounded list must be named. `footer` stays pinned under the rows, outside the scroll, such as a `ListRow` with the count and the total (a row there is not a list item). An empty title or value, an unknown variant, tone or badge tone, a `valueDetail`, `valueTone` or `valueBadge` without a value, a value both toned and a badge, or a bad row count throws a `RangeError`.
836
+
837
+ A row can also carry three trailing parts. `unread` marks it new for the person, such as a document they have not opened: an accent dot after the title's last word (drawn as a border, so forced colors keep it in the text color), hidden from assistive technology, and `unreadLabel` (required while `unread`, such as "New") is read after the title, in a pressable row's name ("Heron calls New"). `tag` is a short status before the row's end, such as a `Badge` ("Only in English"), centred on the row and never wrapped; a pressable row is described by it after its detail lines. `action` is a control after the row and outside its press area, such as an `ActionMenu` of the row's own commands named by the consumer ("More for Heron calls"): it is the row's sibling in the list item, centred on the row, after it in the Tab order, and a press on it never opens the row; it keeps the row's end inset (a space-4 in a panel or outside a ListGroup, none in a plain list), and where `:has` is supported the hover fill spans the row and its control. An empty slot (`undefined`, `null`, `false` or `''`, as conditional JSX gives) draws nothing, for `leading` too. Pair `action` with `accessory="none"` when the row's own chevron would sit beside it. An unread row without `unreadLabel`, or a blank one, throws a `RangeError`. An `aria-label` replaces the whole name, unread words included.
838
+
839
+ ```tsx
840
+ <ListRow
841
+ leading={
842
+ <GlyphTile hue="orange">
843
+ <Feather />
844
+ </GlyphTile>
845
+ }
846
+ title={guide.title}
847
+ detail={guide.detail}
848
+ href={guide.href}
849
+ onPress={() => router.push(guide.href)}
850
+ unread={guide.isNew}
851
+ unreadLabel="New"
852
+ tag={guide.onlyOne ? <Badge>Only in English</Badge> : undefined}
853
+ accessory="none"
854
+ action={
855
+ <ActionMenu
856
+ label={`More for ${guide.title}`}
857
+ trigger={<Ellipsis />}
858
+ align="end"
859
+ items={items}
860
+ />
861
+ }
862
+ />
863
+ ```
664
864
 
665
865
  ```tsx
666
866
  <Card padding={4}>
@@ -732,6 +932,33 @@ Values outside the catalog throw a `RangeError`. There is no per-breakpoint span
732
932
 
733
933
  `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that shows under the label on a phone. Pass `totalLabel` (for example "Total") to name the total column: it renders a `th scope="col"` shown over the totals from `md` up and visually hidden below it, and without it the corner stays an empty cell as before (either way the column takes no width on a phone). `labelWidth` (`xs` to `xl`, the `Table` column sizes) lines a strip's columns up with every other strip of the same columns in a container of the same width, such as the cards of a feed: the row labels take that width and the count columns share the rest of the region equally, so no content moves a column. `labelAppearance="plain"` (default `"tone"`) sets every row label in the text color at label weight with no tone stripe, in both layouts, for a grid whose rows need no color to tell apart; a row's `tone` still colors its total, and an unknown value throws. Three more grid-level options match a framed count sheet, each off by default: `frame="table"` (default `"none"`) draws a strip as a framed table, its scroll region a surface inside a border hairline at the `sm` radius, the heading row on the subtle fill, a rule between columns, the counts at label size centred under their heads and the cells padded a space-3 step across (the tiles layout has no frame and ignores it); `totalEmphasis="strong"` (default `"default"`) sets the row totals in bold label type, a neutral row's in the text color and a toned row's in its tone, under the label on a phone too; and `cellToneAppearance="pill"` (default `"text"`) sets a `cellTones` count of a strip's read-only row on a rounded tint of its tone in the tone's ink (the `Notice` fill and words, 4.5:1 on the tint except where a palette's own `danger` is already under 4.5:1, harvest and mocha dark), outlined in forced colors; entry counts and tiles keep their tone border. A strip row's `valueRow` (`{ label, cells }`, opt-in) adds a line right under it that gives each column's value, such as the money its count adds up to: its own table row on the subtle fill, `label` (such as "Value") in the label column (named with the row's label, "Counted Value", for table navigation), each of `cells` (one consumer-formatted string or `null` per column; `null` or `''` leaves it blank) centred under its count at label size in tabular figures, and the row's `total` moved onto it (bold with `totalEmphasis="strong"`, under the line's name on a phone); the counts' own total cell stays empty. It keeps the row's tone and `disabled` (a disabled line fades its values). Each entry count with a value is described by it (`aria-describedby`, before an error), and the value is no tab stop. The grid still does no arithmetic. A `valueRow` in the tiles layout, a blank label, or cells that are not one string or `null` per column throw a `RangeError`. With `zeroLabel=""` a toned zero or empty count shows nothing in either appearance, so tone only counts that are there. An unknown value of any of them throws a `RangeError`. `zeroLabel` (default `"—"`) is what a zero or empty read-only count shows; an empty string leaves those counts blank, so only the counts that are there stand out (it suits the strip, where the column header names each blank cell, better than tiles, where a blank tile is only its column's name), and a `zeroLabel` that is not a string throws. On a phone the total cell stays in the table, visually hidden, so a screen reader still reads each total in its row; the copy under the label is `aria-hidden`. A strip wider than its container scrolls sideways inside its own keyboard-focusable region, named after `label`, with the row labels pinned at the start and the same edge shade as `Table` while counts are scrolled out of view (the start shade is cast from the pinned labels' edge, since they cover the region's own); the page never scrolls sideways. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. Each tiles row shows its label line (icon, label and `total`) when the grid has several rows, or when a lone row has an icon or a `total`; a lone plain row is named by the grid's `label` alone. Every row is a region named by its `label`, except a lone plain row whose `label` is the grid's own: it has no name of its own, so assistive technology hears the grid's group once rather than a region repeating it. `rowRole="group"` (default `"region"`) keeps every named row's name but makes it a `group` instead of a landmark region, for a page with many grids (one per order card, say) whose rows would otherwise fill the landmark list; the strip ignores it, since its rows are table rows, and an unknown `rowRole` throws a `RangeError` in either layout. A row's icon and its label words stay on one line in both layouts (`sw-denomination-label-line`): a long label wraps its words beside the icon, never under it, and on a phone the strip's total goes on its own line under them. A row's `cellTones` (one Badge tone or `null` per column) tones a single count in place: the count is set in the tone over its signed or zero look, and in the tiles layout its tile's border takes the tone too, so a short bill turns red without leaving its row; `neutral` or `null` leaves a count as it is, a row's `tone` still colors only its label, and a `cellTones` that is not one known tone or `null` per column throws. The tone never says why on its own; put that in words beside the grid. Every negative count, signed or not and in either layout, is written with the typographic minus ("−1"), never a hyphen. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
734
934
 
935
+ `DenominationGrid` board options, all opt-in.
936
+
937
+ - A row's `negativeAppearance="plain"` (`denominationNegativeAppearances`, default `"danger"`) sets its negative counts in the text colour with their minus. Use it for counts that go out by design, such as bills paid out: going out is not an error, so never red. `"danger"` stays for a shortage.
938
+ - `transposeBelow="md"` (or `"lg"`; `denominationTransposeBreakpoints`; the strip layout only) draws the strip transposed below that width, for a phone. Each denomination is a row (its label and caption, pinned while the columns scroll), each grid row a column (its label over its counts, entry steppers or value line), and the totals a last row named by `totalLabel`. Names, entry, cell tones, the zero label and quiet and disabled rows are the strip's; a disabled row says so on its column heads. It reads the width with the breakpoint's media query, so a server render draws the strip, and crossing the breakpoint (turning a phone) remounts the grid, so a count being typed loses focus.
939
+ - `countDigits` (3 to 6, entry rows only) keeps that many digits' room in every entry count before the strip scrolls, such as 4 for counts in the thousands in seven columns. The default room is 3.
940
+ - An unknown appearance or breakpoint, `transposeBelow` on tiles, or a `countDigits` out of range or without entry rows throws a `RangeError`.
941
+
942
+ ```tsx
943
+ <DenominationGrid
944
+ label="Bird rings"
945
+ columns={ringColumns}
946
+ countDigits={4}
947
+ transposeBelow="md"
948
+ totalLabel="Total"
949
+ rows={[
950
+ { id: 'kept', label: 'Keep on hand', cells: kept, entry, valueRow },
951
+ {
952
+ id: 'given',
953
+ label: 'Given out',
954
+ cells: given,
955
+ signed: true,
956
+ negativeAppearance: 'plain',
957
+ },
958
+ ]}
959
+ />
960
+ ```
961
+
735
962
  A strip row with `entry` makes its counts editable: each column holds a `Stepper` (`width="full"`, its label hidden and named by the row label and the column's label and caption as its header reads, such as "Received $20" or "Counted $1 × 100") whose count is the row's `cells` entry (`null` is an empty box). `entry.onChange(columnKey, value)` reports a change; `entry.decrementLabel` and `entry.incrementLabel` name the buttons; `entry.max` caps each column (`null` for none), `entry.errors` puts a message under a column's count (`null` for none), and `entry.placeholder` fills an empty box. `entry.limitAppearance` is the Steppers' `limitAppearance`: `"plain"` keeps − at 0 (and + at a cap) unfaded. The minimum is 0. The grid still does no arithmetic, so recompute the row's `total` in `onChange`. A strip with an entry row centres its heads and read-only counts over the steppers, and each count keeps spacing step 8 of room, so a narrow strip scrolls in its region instead of crushing it. A `cellTones` tone on an entry count draws its border in the tone (danger for a difference); an `accent` tone, for a suggestion or a change, is the `Stepper`'s `changed` look (border and tint); a message in `entry.errors` wins. A row `caption` is a muted line under its label ("from the customer"), and a column `caption` a muted note after its label ("× 100"). Captions show in both layouts. In the strip, `emphasis="quiet"` sets a row on the subtle fill, for a row to compare against, and `disabled` fades a row that does not apply, disables its entry and marks it `aria-disabled`. Entry rows, quiet rows and disabled rows need the `strip` layout. An entry row's counts are zero or more and it is not `signed`; its label is its own among the entry rows, as each column's label and caption are, so every count has its own name. Breaking any of these, a cap or message list that is not one per column, a cap that is not a whole count of zero or more, blank button names or captions, an unknown `emphasis`, or a `disabled` that is not a boolean throws a `RangeError`.
736
963
 
737
964
  ```tsx
@@ -764,11 +991,51 @@ A strip row with `entry` makes its counts editable: each column holds a `Stepper
764
991
 
765
992
  `FilterChips` presents a long, wrapping single-choice filter set. Provide a localized group `label`, controlled `value`, `onChange`, and options with stable values and localized labels. An optional non-negative integer `count` renders as a tabular chicklet after the label and is part of the option's accessible name; a zero count quiets the chip until it is selected: it drops its glass fill and sets its label in the muted color (4.5:1 in every palette), without fading, since it can still be chosen. Native radios provide arrow and Space navigation; disabled options are skipped. A `value` of `''` is nothing chosen yet, for a choice the person must still make, such as a reason. `required` sets `required` on each radio and shows the required mark after the label (hidden from assistive technology). `error` puts a message under the chips, as `Field`'s does (a polite live region kept in the page, out of the layout while empty), links it to the group by `aria-describedby`, sets `aria-invalid` on the group and edges every chip in the danger color (`Mark` in forced colors); an empty string is no error. `aria-describedby` adds your own description ids, such as a hint, before the error. A value that is neither `''` nor an option throws a `RangeError`; a filter set always passes a real option. Inside a `<form>`, the browser checks `required` radios on submit and shows its own message first, so a form that shows `error` itself sets `noValidate`.
766
993
 
994
+ `FilterChips` options for chips that sit above a list, all opt-in: `labelVisuallyHidden` keeps `label` as the group's name without drawing it, for chips whose words and place already say what they filter; `countAppearance="inline"` (`filterChipCountAppearances`, default `"figure"`) writes the count in the chip's own words after a middle dot ("Open · 3"), the dot hidden so the name reads "Open 3"; `overflow="scroll"` (`filterChipsOverflows`, default `"wrap"`) keeps the chips on one row that scrolls sideways inside its own box, never the page, each chip on one line, a focused chip scrolled into view and the focus ring unclipped. `multiple` turns several chips on at once, such as the days of a week: each chip is a native checkbox and its own Tab stop, `value` is a list of the chosen values (empty for none) and `onChange` receives every chosen value in the options' order. A multiple group cannot be `required` (nothing makes a checkbox group required); say what is needed in `error`. A value that is no option, a multiple `value` that is not a list, `required` with `multiple`, or an unknown count appearance or overflow throws a `RangeError`.
995
+
996
+ ```tsx
997
+ <FilterChips
998
+ label="Nocturnal surveys"
999
+ labelVisuallyHidden
1000
+ countAppearance="inline"
1001
+ overflow="scroll"
1002
+ value={nights}
1003
+ onChange={setNights}
1004
+ options={[
1005
+ { value: 'all', label: 'All', count: 7 },
1006
+ { value: 'owls', label: 'Owls heard', count: 3 },
1007
+ ]}
1008
+ />
1009
+
1010
+ <FilterChips
1011
+ label="Watch days"
1012
+ multiple
1013
+ value={watchDays}
1014
+ onChange={setWatchDays}
1015
+ options={[
1016
+ { value: 'fri', label: 'Fri' },
1017
+ { value: 'sat', label: 'Sat' },
1018
+ ]}
1019
+ />
1020
+ ```
1021
+
767
1022
  `DateField` is a controlled date-only field that draws its own calendar. Supply `value`, `min`, and `max` as valid `YYYY-MM-DD` dates; use an empty `value` for a blank field. Its callback returns a date-only string or an empty string, without timezone conversion. Invalid serialized dates and inverted bounds throw; a value outside the bounds remains visible with invalid styling. `required` marks the label with the same `aria-hidden` asterisk as `Field` and sets the entry's native `required`. Its label is drawn as `Field`'s (the same label words and mark inside a `<label>` that keeps the canvas type), so its label row and its gap to the entry match a `Field` beside it. For a blank required field, supply `error` when form validation runs to show the message and invalid styling.
768
1023
 
1024
+ `DateField` `entry="calendar"` (opt-in; `dateFieldEntries`, default `"typed"`) is a day picked, never typed, such as a hire date: one neutral `Button` shaped as a field's control (the small radius, sized to its words on one line at the column's start) holding the calendar glyph and the chosen day in the locale's medium style ("Oct 6, 2026"), or `labels.chooseDate` while the field is empty (pass "Choose a day"). It opens the same calendar, which picks, steps by month and year, picks today and, on an optional field, clears. It is named by the label and its words ("Hire date Choose a day"), described by the `description` and `error`, and a label press opens it too. A button has no required state or text to check, so `required` draws the label's mark and says `labels.required` ("Required") after its name, the form's own check supplies `error`, and the danger edge marks it while `error` is set or the value is outside `min`/`max`. A button cannot be `aria-invalid`, so pass an `error` for a value outside the bounds too: the edge alone is colour only. A long day or `chooseDate` in a narrow column ends in an ellipsis; its name keeps the whole words. An unknown `entry` throws a `RangeError`.
1025
+
1026
+ ```tsx
1027
+ <DateField
1028
+ entry="calendar"
1029
+ label="Banding day"
1030
+ labels={{ chooseDate: 'Choose a day' }}
1031
+ value={bandingDay}
1032
+ onChange={setBandingDay}
1033
+ />
1034
+ ```
1035
+
769
1036
  People type the date into a text entry in the locale's numeric order (`MM/DD/YYYY` for en-US, `DD/MM/YYYY` for es), with any non-digit separator, eight bare digits, or ISO `YYYY-MM-DD`. A keystroke calls `onChange` once the entry is complete, meaning its last field is at full width (four year digits, or two digits for a trailing day or month, as in ISO); otherwise the date commits when the person leaves the field or presses Enter, so typing `2024-03-10` never sends `2024-03-01` on the way. If the parent keeps the old value, the typed text stays on screen; any other outside change replaces it. Text that is not a date keeps the last value and, once the person leaves the field, sets `aria-invalid` and shows `labels.invalidEntry`. Like the native date input, the entry blocks form submission through `setCustomValidity`: `labels.invalidEntry` while its text is not a date, and `labels.outOfRange` while its date, typed or given, is outside `min`/`max`. The calendar button beside it opens a WAI-ARIA date picker dialog anchored under the field on the popover layer: the month grid shows today, the selected day, and days outside `min`/`max` as `aria-disabled` (padding before `0001-01-01` or after `9999-12-31` is blank and inert); arrows move by day and week, Home and End to the week's edges, PageUp and PageDown by month, Shift with them by year, Enter or Space selects and closes, and Escape closes and returns focus to the button. Month and year selectors in the header jump decades (the year list spans 120 years back and 20 ahead of today and the shown month, cut to `min` and `max`; the month list offers only months with a day inside them). A new `value`, `min`, or `max` while the calendar is open moves it to the new value or back inside the bounds. **Today** selects today; **Clear** appears only when the field is not `required`. Today is the device's local date unless `today` (a `YYYY-MM-DD` date) is given: pass the business's own day when it keeps a time zone the device may not share, so a phone in another zone after the business's midnight does not mark tomorrow as today, open on it, or pick it while `max` is the business's day. `today` moves the today mark, the empty field's starting day, **Today**, and the year list's span; it may lie outside `min`/`max`, where **Today** is disabled as before. An empty or malformed `today` throws a `RangeError`.
770
1037
 
771
- `locale` (BCP 47) sets month and weekday names, spoken dates, and the typed order; it defaults to the nearest `lang` attribute when the field mounts, then `en-US`, and a malformed tag throws. `weekStartsOn` is `0` (Sunday, the default) or `1` (Monday). `labels` overrides the control's own words, English by default: `chooseDate`, `previousMonth`, `nextMonth`, `month`, `year`, `today`, `clear`, `invalidEntry`, `outOfRange`, and the placeholder letters `dayPlaceholder`, `monthPlaceholder`, `yearPlaceholder`. The field label names only the text entry; the calendar button is named `labels.chooseDate` and described by the field label, so tests find the entry with `getByLabel` or `getByRole('textbox', { name })`.
1038
+ `locale` (BCP 47) sets month and weekday names, spoken dates, and the typed order; it defaults to the nearest `lang` attribute when the field mounts, then `en-US`, and a malformed tag throws. `weekStartsOn` is `0` (Sunday, the default) or `1` (Monday). `labels` overrides the control's own words, English by default: `chooseDate`, `previousMonth`, `nextMonth`, `month`, `year`, `today`, `clear`, `invalidEntry`, `outOfRange`, `required` (said after a required calendar entry's name; `entry="calendar"` only), and the placeholder letters `dayPlaceholder`, `monthPlaceholder`, `yearPlaceholder`. With the typed entry (the default) the field label names only the text entry; the calendar button is named `labels.chooseDate` and described by the field label, so tests find the entry with `getByLabel` or `getByRole('textbox', { name })`. With `entry="calendar"` the one button is named by the label and its words (`getByRole('button', { name })`), and `chooseDate` is its empty text.
772
1039
 
773
1040
  ```tsx
774
1041
  <DateField
@@ -10,6 +10,8 @@ export declare function monthNames(locale: string): string[];
10
10
  export declare function monthTitle(month: CalendarMonth, locale: string): string;
11
11
  /** A spoken date such as "Sunday, March 10, 2024"; empty for no date. */
12
12
  export declare function formatDateLabel(value: string, locale: string): string;
13
+ /** A date as a field shows it, such as "Mar 10, 2024"; empty for no date. */
14
+ export declare function formatDateMedium(value: string, locale: string): string;
13
15
  /**
14
16
  * An icon-only calendar trigger's name: its purpose, a separator, then the
15
17
  * date it holds spoken in full, such as "Choose survey day, Tuesday,
@@ -30,6 +30,13 @@ export function formatDateLabel(value, locale) {
30
30
  return '';
31
31
  return dateFormatter(locale, { dateStyle: 'full' }).format(formatterDate(parts));
32
32
  }
33
+ /** A date as a field shows it, such as "Mar 10, 2024"; empty for no date. */
34
+ export function formatDateMedium(value, locale) {
35
+ const parts = parseDateOnly(value);
36
+ if (!parts)
37
+ return '';
38
+ return dateFormatter(locale, { dateStyle: 'medium' }).format(formatterDate(parts));
39
+ }
33
40
  /**
34
41
  * An icon-only calendar trigger's name: its purpose, a separator, then the
35
42
  * date it holds spoken in full, such as "Choose survey day, Tuesday,